FIELDimageR pipeline:
Phenomics applied to plant breeding
Introduction
Agriculture and plant breeding
Images can be used in plant phenotyping to draw inference about many traits:
- Geometric traits (i.e. plant height, leaf area index, lodging, crop canopy cover)
- Canopy spectral texture (spectral features)
- Physiological traits (i.e., chlorophyll, biomass, pigment content, photosynthesis)
- Abiotic/biotic stress indicators (i.e., stomatal conductance, canopy temperature difference, leaf water potential, senescence index)
- Nutrients (nitrogen concentration, protein content)
- Yield
FIELDimageR
FIELDimageR is a R package to analyze images and plant phenotyping, and allows to:
- Crop the image
- Remove soil effect
- Build vegetation indices
- Rotate the image
- Build the plot shapefile
- Extract information for each plot
- Evaluate stand count, canopy percentage, and plant height
FIELDimageR pipeline
1) Example 01
Remote sensing - RGB and DSM - (Potato Breeding)
Jeffrey Endelman and Filipe Matias (UW-Madison)
Required packages
## Installing
# install.packages("sp")
# install.packages("raster")
# install.packages("rgdal")
# install.packages("ggplot2")
# install.packages("agricolae")
# install.packages("reshape2")
# install.packages("devtools")
# install.packages("ggrepel")
# install.packages("lme4")
# install.packages("plyr")
# install.packages("DescTools")
# devtools::install_github("filipematias23/FIELDimageR")
## Necessary packages
library(FIELDimageR)
library(raster)
library(agricolae)
library(reshape2)
library(ggplot2)
library(lme4)
library(plyr)
library(ggrepel)
library(DescTools)
Data (R/FIELDimageR)
- Potato breeding: 140 plots (3x15 feet)
- Population: 97 genotypes
- Six UAV flights: 30 (soil base), 40, 70, and 100 days after planting (DAP)
Basic steps
# Rotating image with theta 2.3:
Test.Rotate<-fieldRotate(Test.Crop, theta = 2.3)
#Test.Rotate<-fieldRotate(Test.Crop, clockwise = F)
Building the plot shapefile
- Dataset - Field notes - phenotype (Download: ‘EX1_Data.csv’)
# Making the field Map
Map<-fieldMap(fieldPlot = Data$Plot,
fieldColumn = Data$Column,
fieldRow = Data$Row,
decreasing = T)
Map - EX1 field design (MAP) should start from bottom to top and left to the right. In other words, plot 1 should be where the blue arrow is showing in the image below.
# Rotating the MAP to correct plots position:
rotate <- function(x) t(apply(x, 2, rev))
Map<-rotate(rotate(Map))
Map# Building the plot shapefile (ncols = 14 and nrows = 10)
# x11()
plotShape<-fieldShape(mosaic = Test.RemSoil$newMosaic,
ncols = 14,
nrows = 10,
fieldData = Data,
ID = "Plot",
fieldMap = Map)# Building indices ("NGRDI","GLI")
Test.Indices<- fieldIndex(mosaic = Test.RemSoil$newMosaic,
Red = 1, Green = 2, Blue = 3,
index = c("NGRDI","GLI"),
myIndex = c("(Red-Blue)/Green"))
Extracting Data
Test.Info<- fieldInfo(mosaic = Test.Indices[[c("NGRDI","GLI","myIndex")]],
fieldShape = plotShape$fieldShape,
buffer = -0.10,
n.core = 3)
Test.Info$fieldShape@data
Estimation of plant height using digital surface model (DSM).
# Uploading files from soil base and vegetative growth:
DSM0 <- stack(paste("./DSM/",DSM[1],sep = ""))
DSM1 <- stack(paste("./DSM/",DSM[3],sep = ""))
plot(DSM1)# Cropping the image using the previous shape:
DSM0.Crop <- fieldCrop(mosaic = DSM0,fieldShape = Test.Crop)
DSM1.Crop <- fieldCrop(mosaic = DSM1,fieldShape = Test.Crop)
# Canopy Height Model (CHM):
DSM0.R <- resample(DSM0.Crop, DSM1.Crop)
CHM <- DSM1.Crop-DSM0.R
plot(CHM)# Rotating the image using the same theta from RGB mosaic:
CHM.Rotate<-fieldRotate(CHM, theta = 2.3)
# Removing the soil using the mask from RGB mosaic:
CHM.RemSoil <- fieldMask(CHM.Rotate, mask = Test.RemSoil$mask)dev.off()
par(mfrow=c(1,3))
# Profile plot:
plot(x = CHM.Draw$Draw1$drawData$x-100230,
y = CHM.Draw$Draw1$drawData$layer,
type="l", col="red",lwd=1,ylim=c(0,1),
xlab="Distance (m)", ylab="EPH (m)")
lines(x = CHM.Draw$Draw2$drawData$x-100230,
y = CHM.Draw$Draw2$drawData$layer,
type="l", col="blue",lwd=1,add=T)
# CHM plot:
plot(CHM.Rotate, col = grey(1:100/100), axes = FALSE, box = FALSE,legend=F)
lines(CHM.Draw$Draw1$drawData$x,CHM.Draw$Draw1$drawData$y, type="l", col="red",lwd=2)
lines(CHM.Draw$Draw2$drawData$x,CHM.Draw$Draw2$drawData$y, type="l", col="blue",lwd=2)
#RGB plot:
plotRGB(Test.Rotate)
lines(CHM.Draw$Draw1$drawData$x,CHM.Draw$Draw1$drawData$y, type="l", col="red",lwd=2)
lines(CHM.Draw$Draw2$drawData$x,CHM.Draw$Draw2$drawData$y, type="l", col="blue",lwd=2)# Extracting the estimate plant height average (EPH):
EPH <- fieldInfo(CHM.RemSoil$newMosaic,
fieldShape = Test.Info$fieldShape,
fun = "mean") #fun="quantile"
EPH$plotValueEvaluating all mosaics in a loop
DataTotal<-NULL
for(i in 2:length(MOSAIC)){
EX1 <- stack(paste("./MOSAIC/",MOSAIC[i],sep = ""))
EX1.Crop <- fieldCrop(mosaic = EX1,
fieldShape = Test.Crop,
plot = F)
EX1.Rotate<-fieldRotate(EX1.Crop,
theta = 2.3,
plot = F)
EX1.RemSoil<-fieldMask(EX1.Rotate, plot = F)
EX1.Indices<- fieldIndex(mosaic = EX1.RemSoil$newMosaic,
Red = 1, Green = 2, Blue = 3,
index = c("NGRDI","BGI", "GLI","VARI"),
myIndex = c("(Red-Blue)/Green"),
plot = F)
EX1.Info<- fieldInfo(mosaic = EX1.Indices[[c("NGRDI","BGI", "GLI","VARI","myIndex")]],
fieldShape = plotShape$fieldShape,
n.core = 3)
DSM0 <- stack(paste("./DSM/",DSM[1],sep = ""))
DSM1 <- stack(paste("./DSM/",DSM[i],sep = ""))
DSM0.Crop <- fieldCrop(mosaic = DSM0,fieldShape = Test.Crop,plot = F)
DSM1.Crop <- fieldCrop(mosaic = DSM1,fieldShape = Test.Crop,plot = F)
DSM0.R <- resample(DSM0.Crop, DSM1.Crop)
CHM <- DSM1.Crop-DSM0.R
CHM.Rotate<-fieldRotate(CHM, theta = 2.3,plot = F)
CHM.RemSoil <- fieldMask(CHM.Rotate, mask = EX1.RemSoil$mask,plot = F)
EPH <- fieldInfo(CHM.RemSoil$newMosaic,
fieldShape = EX1.Info$fieldShape,
fun = "mean")
DataTotal<-rbind(DataTotal,
data.frame(DAP=as.character(do.call(c,strsplit(MOSAIC[i],split = "_"))[2]),
EPH$fieldShape@data))
# Making map plots
fieldPlot(fieldShape=EPH$fieldShape,
fieldAttribute="NGRDI",
mosaic=EX1.Rotate,
color=c("red","green"),
# min.lim = 0,
# max.lim = 0.35,
alpha = 0.5,
round = 2)
print(paste("### Completed: ", "Mosaic_",i," ###",sep=""))
}
# Organizing the data table:
colnames(DataTotal)<-c(colnames(DataTotal)[-c(dim(DataTotal)[2])],"EPH") # layer=EPH
DataTotal<-DataTotal[,!colnames(DataTotal)%in%c("ID","ID.1","PlotName")] # Removing column 12 ("ID.1")
DataTotal
# Saving extracted data "DataTotal.csv":
write.csv(DataTotal,"DataTotal.csv",row.names = F,col.names = T)
- Graphics
DataTotal$Name<-as.factor(as.character(DataTotal$Name))
DataTotal$Row<-as.factor(as.character(DataTotal$Row))
DataTotal$Column<-as.factor(as.character(DataTotal$Column))
DataTotal$DAP<-as.numeric(as.character(DataTotal$DAP))
DataTotal$NGRDI<-as.numeric(as.character(DataTotal$NGRDI))
DataTotal$BGI<-as.numeric(as.character(DataTotal$BGI))
DataTotal$EPH<-as.numeric(as.character(DataTotal$EPH))
ggplot(DataTotal, aes(x = NGRDI,fill=as.factor(DAP))) +
geom_density(alpha=.5,position = 'identity') +
facet_wrap(~DAP,ncol = 1)+
scale_fill_grey(start=1, end=0)+
labs(y="#genotypes",x="NGRDI", fill="DAP") +
theme_bw()
- Linear Regression
DataTotal.Reg<-subset(DataTotal,DAP=="40")
DataTotal.Reg$Check<-as.character(DataTotal.Reg$Name)
DataTotal.Reg$Check[!DataTotal.Reg$Check%in%c("G43","G44","G45")]<-""
ggplot(DataTotal.Reg,aes(y=NGRDI, x=EPH)) +
geom_point() +
geom_smooth(method=lm)+
labs(y="EPH",x="NGRDI",fill="",alpha="")+
geom_vline(aes(xintercept=0.02),col="red", linetype = 2, size=0.7) +
theme_bw()+
geom_text_repel(aes(label = Check),
size = 3.5, col="red",
box.padding = 0.35,
point.padding = 0.5,
segment.color = 'grey50')+
theme(legend.position = "none",
panel.grid.major = element_blank(),
panel.grid.minor = element_blank())
- Heritability
DAP<-unique(DataTotal$DAP)
H2<-NULL
for(h in 1:length(DAP)){
mod<-lmer(NGRDI~Row+Column+(1|Name),DataTotal[as.character(DataTotal$DAP)==DAP[h],])
H2.a<-c("NGRDI",DAP[h],as.data.frame(VarCorr(mod))$vcov[1]/sum(as.data.frame(VarCorr(mod))$vcov))
mod<-lmer(EPH~Row+Column+(1|Name),DataTotal[as.character(DataTotal$DAP)==DAP[h],])
H2.b<-c("EPH",DAP[h],as.data.frame(VarCorr(mod))$vcov[1]/sum(as.data.frame(VarCorr(mod))$vcov))
H2<-rbind.data.frame(H2,rbind(H2.a,H2.b))
}
colnames(H2)<-c("Trait","DAP","H2")
H2$H2<-as.numeric(as.character(H2$H2))
H2$DAP<-as.numeric(as.character(H2$DAP))
ggplot(H2,aes(x=as.factor(DAP),y=H2,fill=as.factor(DAP)))+
geom_bar(stat="identity")+
facet_wrap(~Trait)+
scale_fill_grey(start=0.8, end=0.2)+
labs(x="Days After Planting (DAP)", fill="")+
geom_text(aes(label=round(H2,2)), vjust=1.6, color="white", size=6)+
theme_bw()+
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5))
Area under the curve (AUC)
DataTotal1<-DataTotal[as.character(DataTotal$Name)%in%c("G43","G44","G45"),]
ggplot(data=DataTotal1, aes(x=as.numeric(DAP), y= NGRDI, col= Name, group=Name)) +
geom_point(size=6)+
geom_line(size=1.2) +
scale_color_grey(start=0.8, end=0.2)+
labs(x="Days After Planting (DAP)", fill="", col="")+
theme_linedraw()Trait<-c("NGRDI") # c("GLI","EPH")
Plot<-as.character(unique(DataTotal$Plot))
DataAUC<-NULL
for(a1 in 1:length(Plot)){
D1<-DataTotal[as.character(DataTotal$Plot)==Plot[a1],]
x1<-c(0,as.numeric(D1$DAP))
y1<-c(0,as.numeric(D1[,Trait]))
DataAUC <- rbind(DataAUC,
c(NGRDI_AUC=AUC(x = x1[!is.na(y1)], y = y1[!is.na(y1)]),
Name=unique(as.character(D1$Name)),
Trait=unique(D1$Trait),
Row=unique(D1$Row),
Column=unique(D1$Column)))}
DataAUC<-as.data.frame(DataAUC)
DataAUC$NGRDI_AUC<-as.numeric(as.character(DataAUC$NGRDI_AUC))
DataAUC$Name<-as.factor(DataAUC$Name)
DataAUC$Row<-as.factor(DataAUC$Row)
DataAUC$Column<-as.factor(DataAUC$Column)
DataAUC - AUC Heritability
# Mixed model:
mod<-lmer(NGRDI_AUC~Row+Column+(1|Name),DataAUC)
H2<-as.data.frame(VarCorr(mod))$vcov[1]/sum(as.data.frame(VarCorr(mod))$vcov)
H2
ggplot(DataAUC, aes(x = NGRDI_AUC)) +
geom_histogram(aes(y=..density..), colour="black", fill="white")+
geom_density(alpha=.5,position = 'identity', fill="cadetblue") +
labs(y="#genotypes",x=paste("Area under the curve (AUC_NGRDI: H2=",round(H2,2),")",sep="")) +
theme_bw()
2) Example 02
Hyperspectral (Potato Breeding)
Jeffrey Endelman and Filipe Matias (UW-Madison)
Data Info (R/FIELDimageR)
- Potato breeding: 196 plots (3x15 feet)
- Population: 138 genotypes
- HTP Platform (Dr. Philip Townsend UM-Madison): Cessna-180 airplane + HySpex VNIR-1800 + HySpex SWIR-384
- Hyperspectral Data: 474 bands
Donwload Data:
- Hyperspectral Image ‘EX_HYP.tif’
- Wavelengths Names ‘namesHYP.csv’
## Uploading hyperspectral file with 474 bands (EX_HYP.tif):
EX.HYP<-stack("EX_HYP.tif")
## Wavelengths (namesHYP.csv):
NamesHYP<-as.character(read.csv("namesHYP.csv")$NameHYP)
names(EX.HYP)<-NamesHYP
head(NamesHYP)Building RGB image from the hyperspectral:
# RGB from hyperspectral:
R<-EX.HYP$X654.788879 # 654nm (Red)
G<-EX.HYP$X552.598572 # 552nm (Green)
B<-EX.HYP$X450.408295 # 450nm (Blue)
RGB<-stack(c(R,G,B))
plotRGB(RGB, stretch="lin")Making the mask by using the RGB to calculate vegetation indices and to remove the soil
## Removing soil using RGB (index NGRDI):
RGB.S<-fieldMask(RGB,index="NGRDI",cropValue = 0.0, cropAbove = F)Building the plot shapefile
- Dataset - Field notes - phenotype (Download: ‘DataHYP.csv’)
## Data frame with field information to make the Map:
Data<-as.data.frame(read.csv("DataHYP.csv"))
head(Data)- EX.HYP field design (MAP) should start from top to bottom and right to left. In other words, plot 1 should be where the blue arrow is showing in the image below.
Map<-fieldMap(fieldPlot = as.character(Data$Plot),fieldRow = as.character(Data$Range),fieldColumn = as.character(Data$Row),decreasing = T)
Map## Building plot shapefile using RGB as base ( 14 columns and 14 rows):
plotFile<-fieldShape(RGB.S,ncols = 14, nrows = 14,
fieldMap = Map,fieldData = Data, ID = "Plot")Removing soil from the hyperspectral using the mask from the RGB (Time: 5-10 min). Download the final result: ‘EX_HYP_S.tif’
## Removing soil from the hyperspectral:
# EX.HYP.S<-fieldMask(EX.HYP,mask = RGB.S$mask, plot = F)
# writeRaster(EX.HYP.S$newMosaic, filename="EX_HYP_S.tif", options="INTERLEAVE=BAND", overwrite=TRUE)
EX.HYP.S<-stack("EX_HYP_S.tif")
names(EX.HYP.S)<-NamesHYP
plot(EX.HYP.S$X750.592224, col=grey(100:1/100), axes=FALSE, box=FALSE)
plot(plotFile$fieldShape, add=T)
## Reducing resolution to accelerate data extraction por plot (e.g., average values):
EX.HYP.S<-aggregate(EX.HYP.S,2)
plot(EX.HYP.S$X750.592224, col=grey(100:1/100), axes=FALSE, box=FALSE)
plot(plotFile$fieldShape, add=T)Extracting data from 474 bands (Time: 3-10 min).
## Extracting data (474 bands):
EX.HYP.I<-fieldInfo(EX.HYP.S, # EX.HYP.S$newMosaic,
fieldShape = plotFile$fieldShape,
n.core = 1)
## Saving the new csv with hyperspectral information per plot:
DataHYP<-EX.HYP.I$fieldShape@data
colnames(DataHYP)<-c(colnames(DataHYP)[1:9],NamesHYP)
DataHYP[1:5,1:12]
# write.csv(DataHYP,"DataHypNew.csv",col.names = T,row.names = F)Making graphics to visualize the data:
## Visualizing the extracted data from FIELDimageR:
dev.off()
DataHYP1<-EX.HYP.I$plotValue[,-1]
## Plotting the data:
plot(x=as.numeric(NamesHYP),y=as.numeric(DataHYP1[1,]),type = "l",xlab = "Wavelength (nm)",ylab = "Reflectance",
col="black",lwd=2,cex.lab=1.2)
for(i in 2:dim(DataHYP1)[1]){
lines(x=as.numeric(NamesHYP),y=as.numeric(DataHYP1[i,]),type = "l",col=i,lwd=2)
}
legend("topright",
c("Blue (400-480nm)",
"Green (480-600nm)",
"Red (600-680nm)",
"RedEdge (680-750nm)",
"NIR (750-1300nm)",
"SWIR-1 (1500-1800nm)",
"SWIR-2 (2000-2400nm)"),
col =c("black"),box.lty=0,cex = 0.8)“Best practices” to use hyperspectral data:
- Removing atmospheric absorption regions
- Vector normalization to reduce the effects of illumination conditions (Feilhauer et al., 2010)
## Removing atmospheric absorption regions:
DataHYP2<-DataHYP[,-c(1:9)]
Wavelenght<-as.numeric(colnames(DataHYP2))
DataHYP2<-DataHYP2[,!(Wavelenght>1290&Wavelenght<1510|
Wavelenght>1790&Wavelenght<2040|
Wavelenght>2350&Wavelenght<2550)]
## Vector normalization to reduce the effects of illumination conditions:
DataHYP2<-as.matrix(t(apply(DataHYP2, 1, function(x){
x/sqrt(sum(as.numeric(x)^2))
})))
## Plotting the new data:
plot(x=as.numeric(colnames(DataHYP2)),y=as.numeric(DataHYP2[1,]),type = "p",
xlab = "Wavelength (nm)",ylab = "Reflectance",
col="black",pch=19,cex.lab=1.2,
cex=0.5)
for(i1 in 2:dim(DataHYP2)[1]){
lines(x=as.numeric(colnames(DataHYP2)),y=as.numeric(DataHYP2[i1,]),type = "p",col=i1,pch=19,cex=0.5)
}
legend("topright",
c("Blue (400-480nm)",
"Green (480-600nm)",
"Red (600-680nm)",
"RedEdge (680-750nm)",
"NIR (750-1300nm)",
"SWIR-1 (1500-1800nm)",
"SWIR-2 (2000-2400nm)"),
col =c("black"),box.lty=0,cex = 0.8)
## Saving the new csv table:
DataHYP3<-cbind(DataHYP[,c(1:9)],DataHYP2)
# write.csv(DataHYP3,"DataHypNew2.csv",col.names = T,row.names = F)Correlation (r) between wavelengths and field manual collected data (e.g., yield, plant height, etc.)
## Scale the wavelengths values to correct for any unit effects:
DataHYP4<-scale(DataHYP2,scale = T)
## Correlation between wavelengths and Trait_1:
r.Hyp<-NULL
for(r in 1:dim(DataHYP4)[2]){
r.Hyp<-c(r.Hyp,cor(DataHYP4[,r],scale(DataHYP$Trait_1,scale = T),use = "pairwise.complete.obs"))
}
## Plotting correlation values:
plot(x=as.numeric(colnames(DataHYP4)),y=r.Hyp,type = "o",
xlab = "Wavelength (nm)",
ylab = "r",
col="black",pch=19,cex.lab=1.2
)
abline(h=0,col="red",lty=2,lwd=3)
legend("topright",
c("Blue (400-480nm)",
"Green (480-600nm)",
"Red (600-680nm)",
"RedEdge (680-750nm)",
"NIR (750-1300nm)",
"SWIR-1 (1500-1800nm)",
"SWIR-2 (2000-2400nm)"),
col =c("black"),box.lty=0,cex = 0.8)Estimating traits’ heritability
## Preparing the data:
str(DataHYP3)
DataHYP3$Name<-as.factor(DataHYP3$Name)
DataHYP3$Block<-as.factor(DataHYP3$Block)
Trait<-c("Trait_1",paste0("`",colnames(DataHYP3)[-(1:9)],"`",sep="")) - Using mixed models to evaluate the data and estimate the heritability
## Using the package "lme4" for mixed modeling:
H2<-NULL
for(t in 1:length(Trait)){
mod<-lmer(eval(parse(text = paste(Trait[t],"~Block+(1|Name)",sep=""))),data = DataHYP3)
H2<-c(H2,c(as.data.frame(VarCorr(mod))$vcov[1]/sum(as.data.frame(VarCorr(mod))$vcov)))
}
Trait_1.H2<-H2[1]
Trait_1.H2 # H2=81%
## Plotting heritability values:
plot(x=as.numeric(colnames(DataHYP2)),y=as.numeric(H2[-1]),type = "o",
xlab = "Wavelength (nm)",ylab = "H2",
col="black",pch=19,cex.lab=1.2,
ylim=c(0.1,0.9),
cex=0.5)
abline(h=Trait_1.H2,col="blue",lty=2,lwd=3)
text(x = 1300, y = (Trait_1.H2+0.03), paste("Trait_1 (H2=",round(Trait_1.H2,2),")",sep=""),col="blue")
legend(list(x = 1600,y = (Trait_1.H2-0.03)),
c("Blue (400-480nm)",
"Green (480-600nm)",
"Red (600-680nm)",
"RedEdge (680-750nm)",
"NIR (750-1300nm)",
"SWIR-1 (1500-1800nm)",
"SWIR-2 (2000-2400nm)"),
col =c("black"),box.lty=0,cex = 0.8)Reference
Matias, F.I. (2019). FIELDimageR Pipeline (tutorial). https://github.com/filipematias23/FIELDimageR
Matias, F.I.; Caraza-Harter, M.; Endelman, J.B. (2020). FIELDimageR: A R Package to Analyze Orthomosaic Images from Agricultural Field Trials. The Plant Phenome Journal. DOI:10.1002/ppj2.20005